/*
 * Copyright (c) 2013-2018 上海汇数数据. All rights reserved.
 * @(#) RegionUpdate.java 2018-08-19 16:44
 */

package cn.jh.common.core.cn;

import cn.jh.common.core.io.PathKt;
import cn.jh.common.core.json.JsonKt;
import com.fasterxml.jackson.databind.type.CollectionType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multimap;
import com.google.common.collect.Ordering;
import com.google.common.collect.TreeMultimap;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectOutputStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

import static cn.jh.common.core.cn.RegionConstants.*;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.READ;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import static java.nio.file.StandardOpenOption.WRITE;


/**
 * Region 数据更新工具。
 *
 * @author Fuchun
 * @since 1.0
 */
class RegionUpdate {

    static final Companion Companion = new Companion();
    private static final RegionDataWriter DataWriter = new RegionDataWriter();

    static List<Region> listState() {
        return listRegions(Companion.stateMap.values());
    }

    /**
     * 返回一个不可变的，按照{@code Region::getCode} 顺序排序的二级行政区域列表信息{@code List<Region>}。
     */
    static List<Region> listCity() {
        return listRegions(Companion.cityMap.values());
    }

    /**
     * 返回一个不可变的，按照{@code Region::getCode} 顺序排序的三级行政区域列表信息{@code List<Region>}。
     */
    static List<Region> listCounty() {
        return listRegions(Companion.countyMap.values());
    }

    static List<Region> findChildren(String parent) {
        String pc;
        if (parent == null || (pc = parent.trim()).isEmpty()) {
            return ImmutableList.of();
        }
        Collection<String> listCodes = null;
        Map<String, Region> regionMap = null;
        if (pc.endsWith(STATE_CODE_SUFFIX)) {
            listCodes = Companion.state2CityMap.get(pc);
            regionMap = Companion.cityMap;
        } else if (pc.endsWith(CITY_CODE_SUFFIX)) {
            listCodes = Companion.city2CountyMap.get(pc);
            regionMap = Companion.countyMap;
        }
        if (listCodes == null || listCodes.isEmpty()) {
            return ImmutableList.of();
        }
        ImmutableList.Builder<Region> builder = ImmutableList.builder();
        for (String code : listCodes) {
            builder.add(regionMap.get(code));
        }
        return builder.build();
    }

    static void updateRegionsDat() {
        DataWriter.writeData();
    }

    protected static class Companion {

        private final ReentrantLock lock = new ReentrantLock();

        final Map<String, Region> stateMap;
        final Map<String, Region> cityMap;
        final Map<String, Region> countyMap;
        final Multimap<String, String> state2CityMap;
        final Multimap<String, String> city2CountyMap;

        Companion() {
            stateMap = new HashMap<>(STATE_COUNT);
            cityMap = new HashMap<>(CITY_COUNT);
            countyMap = new HashMap<>(COUNTY_COUNT);

            state2CityMap = HashMultimap.create(STATE_COUNT, 24);
            city2CountyMap = HashMultimap.create(CITY_COUNT, 24);
        }

        void loadRegionData() {
            if (!(stateMap.isEmpty() && cityMap.isEmpty() && countyMap.isEmpty())) {
                return;
            }
            lock.lock();
            try {
                if (stateMap.isEmpty() && cityMap.isEmpty() && countyMap.isEmpty()) {
                    loadStates();
                    loadCities();
                    loadCounties();
                }
            } finally {
                lock.unlock();
            }
        }

        void destroy() {
            lock.lock();
            try {
                stateMap.clear();
                cityMap.clear();
                countyMap.clear();
                state2CityMap.clear();
                city2CountyMap.clear();
            } finally {
                lock.unlock();
            }
        }

        private void loadStates() {
            List<Region> states = loadRegions(RegionConstants.STATES_RES_PATH);
            for (Region state : states) {
                stateMap.put(state.getCode(), state);
            }
        }

        private void loadCities() {
            List<Region> cities = loadRegions(RegionConstants.CITIES_RES_PATH);
            for (Region city : cities) {
                cityMap.put(city.getCode(), city);
                if (city.getParent() != null)
                    state2CityMap.put(city.getParent(), city.getCode());
            }
        }

        private void loadCounties() {
            List<Region> counties = loadRegions(RegionConstants.COUNTIES_RES_PATH);
            for (Region county : counties) {
                countyMap.put(county.getCode(), county);
                if (county.getParent() != null) {
                    city2CountyMap.put(county.getParent(), county.getCode());
                }
            }
        }

        private List<Region> loadRegions(String srcPath) {
            // src/main/resources{srcPath}
            Path path = PathKt.resourcePath(srcPath);
            try (InputStream inputStream = Files.newInputStream(path, READ)) {
                CollectionType listType = TypeFactory.defaultInstance()
                        .constructCollectionType(List.class, Region.class);
                return JsonKt.readValue(inputStream, listType);
            } catch (IOException ex) {
                throw new UncheckedIOException(ex);
            }
        }
    }

    static class RegionDataWriter {

        private final byte nl = '\n';
        private final byte tab = '\t';
        private final byte colon = ':';
        private final byte comma = ',';

        RegionDataWriter() {}

        void writeData() {
            Companion.loadRegionData();
            Path regionsDatPath = PathKt.resourcePath(RegionConstants.REGIONS_RES_PATH);
            try (DataOutputStream output = new DataOutputStream(
                    Files.newOutputStream(regionsDatPath, CREATE, TRUNCATE_EXISTING, WRITE))) {
                writeStates(output);
                writeNonStates(output, RegionLevel.CITY, listCity());
                writeNonStates(output, RegionLevel.COUNTY, listCounty());
            } catch (IOException ex) {
                throw new UncheckedIOException(ex);
            }
        }

        private void writeRegion(DataOutputStream output, Region r) throws IOException {
            output.writeUTF(r.getCode());
            output.writeUTF(r.getName());
            output.writeUTF(r.getFullName());
            output.writeByte(comma); // splitter symbol: comma -> ,
        }

        void writeStates(DataOutputStream output) throws IOException {
            List<Region> stateList = listState();
            output.writeUTF(RegionLevel.STATE.value);
            output.writeShort(stateList.size());
            output.writeByte(colon);
            for (Region region : stateList) {
                writeRegion(output, region);
            }
            output.writeByte(tab); // splitter symbol: tab -> \t
        }

        void writeNonStates(DataOutputStream output, RegionLevel level, List<Region> regionList) throws IOException {
            TreeMultimap<String, Region> parent2RegionsMap = TreeMultimap.create(
                    Ordering.natural(), CODE_COMPARATOR);
            for (Region region : regionList) {
                parent2RegionsMap.put(region.getParent(), region);
            }
            output.writeUTF(level.value);
            int childrenSize = parent2RegionsMap.keySet().size();
            output.writeShort(childrenSize);
            output.writeByte(colon);

            parent2RegionsMap.asMap().forEach((parent, regions) -> {
                try {
                    output.writeShort(regions.size());
                    output.writeUTF(parent);
                    output.writeByte(colon);
                    for (Region region : regions) {
                        writeRegion(output, region);
                    }
                    output.writeByte(nl);
                } catch (IOException ex) {
                    throw new UncheckedIOException(ex);
                }
            });
            output.writeByte(tab); // splitter symbol: tab -> \t
        }
    }

    public static void main(String[] args) {
        updateRegionsDat();
    }
}
